﻿//***********************************************************************************************
//* File Name	: CaptchaControl.cs
//* Description	: This class is main user interface elements, which provide all user interaction
//* 
//* Author		: Ashutosh Phoujdar(ashu.fouzdar@gmail.com)
//* Date		: 15-November-2015
//* ***********************************************************************************************

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Web;
using System.Globalization;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Drawing.Drawing2D;

namespace CaptchaWebControl
{
    public class CaptchaControl : WebControl, INamingContainer, IPostBackDataHandler, IValidator
    {
        private bool _captchaValidated = true;
        private Int32 _captchaImageHeight = 40;
        private Int32 _captchaImageWidth = 150;
        private Int32 _captchaTextMinLength = 5;
        private Int32 _captchaTextMaxLength = 7;
        private string _captchaInputCSS = string.Empty;
        private string _captchaMessageCSS = string.Empty;
        private string _captchaImageCSS = string.Empty;

        private int angleForPreviousChar = 0;
        private string _errorMessage = String.Empty;
        private readonly string _captchaId = Guid.NewGuid().ToString("N");
        private string _prevCaptchaId;
        //This is allowed list of fonts
        private List<string> _allowedFontList = new List<string>{ 
                                                            "arial",
                                                            "arial black", 
                                                            "comic sans ms",
                                                            "courier new",
                                                            "franklin gothic medium",
                                                            "georgia",
                                                            "lucida console",
                                                            "lucida sans unicode",
                                                            "microsoft sans serif", 
                                                            "tahoma",
                                                            "times new roman",
                                                            "verdana"
                                                          };
        /// <summary>
        /// Check if is in design mode
        /// </summary>
        private static bool IsDesignMode
        {
            get { return HttpContext.Current == null; }
        }
        
        #region IPostBackDataHandler Members
        
        public bool LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
        {
            ValidateEnteredCaptcha(Convert.ToString(postCollection[UniqueID], CultureInfo.CurrentCulture));
            return false;
        }

        public void RaisePostDataChangedEvent()
        {
            
        }

        #endregion

        #region Captcha Control properties

        [Browsable(false), Bindable(true), Category("Appearance"),
        DefaultValue("The text you typed does not match the text in the image."),
        Description("Message to display in a Validation Summary when the CAPTCHA fails to validate.")]
        public string ErrorMessage
        {
            get
            {
                if (!_captchaValidated)
                {
                    return _errorMessage;
                }
                return String.Empty;
            }
            set { _errorMessage = value; }
        }

        public override bool Enabled
        {
            get { return base.Enabled; }
            set
            {
                base.Enabled = value;
                if (!value)
                {
                    _captchaValidated = true;
                }
            }
        }

        [Description("Returns True if the user was CAPTCHA validated after a postback."), Category("Captcha")]
        public bool IsCaptchaValidated
        {
            get { return _captchaValidated; }
        }

        [Description("Get or Set Captcha Image Height"), Category("Captcha")]
        public Int32 CaptchaImageHeight
        {
            get { return _captchaImageHeight; }
            set
            {
                _captchaImageHeight = value;
            }
        }

        [Description("Get or Set Captcha Image Width"), Category("Captcha")]
        public Int32 CaptchaImageWidth
        {
            get { return _captchaImageWidth; }
            set
            {
                _captchaImageWidth = value;
            }
        }

        [Description("Get or Set minimum text length of Captcha text"), Category("Captcha")]
        public Int32 CaptchaTextMinLength
        {
            get { return _captchaTextMinLength; }
            set
            {
                if (value < 1)
                    throw new ArgumentOutOfRangeException("Minimum text length for CAPTCHA must be 1 or more");
                _captchaTextMinLength = value;
            }
        }

        [Description("Get or Set maximum text length of Captcha text"), Category("Captcha")]
        public Int32 CaptchaTextMaxLength
        {
            get { return _captchaTextMaxLength; }
            set
            {
                if (value > 35)
                    throw new ArgumentOutOfRangeException("Maximum text length for CAPTCHA must be 35 or less");
                _captchaTextMaxLength = value;
            }
        }

        [Description("Get or Set CSS class for Captcha Input Control"), Category("Captcha")]
        public string CaptchaInputCSS
        {
            get { return _captchaInputCSS; }
            set
            {
                _captchaInputCSS = value;
            }
        }

        [Description("Get or Set CSS class for Captcha message"), Category("Captcha")]
        public string CaptchaMessageCSS
        {
            get { return _captchaMessageCSS; }
            set
            {
                _captchaMessageCSS = value;
            }
        }

        [Description("Get or Set CSS class for Captcha Image"), Category("Captcha")]
        public string CaptchaImageCSS
        {
            get { return _captchaImageCSS; }
            set
            {
                _captchaImageCSS = value;
            }
        }
        
        [Description("Get or Set Font list for Captcha text, Please make sure that font must exist on application environment"), Category("Captcha")]
        public List<string> FontList
        {
            get { return _allowedFontList; }
            set
            {
                if (value.Count < 1)
                    throw new ArgumentException("Invalid font list selected, Please provide atleast font in list");
                _allowedFontList = value;
            }
        }

        #endregion

        #region IValidator Members

        [Browsable(false), Category("Behavior"), DefaultValue(true), Description("Is Valid"),
         DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public bool IsValid
        {
            get { return _captchaValidated; }
            set { }
        }

        public void Validate()
        {
            //-- Validation is done in LoadPostData override
        }

        #endregion
        
        #region Page Lifecycle Overrides

        /// <summary>
        /// Render Captcha control HTML to the page
        /// </summary>
        protected override void Render(HtmlTextWriter writer)
        {
            string captchaString = Convert.ToString(HttpContext.Current.Session[_captchaId]);

            if (!String.IsNullOrEmpty(CssClass))
                writer.AddAttribute(HtmlTextWriterAttribute.Class, CssClass);
            string inheritedStyle = GetStyleTag();
            if (!string.IsNullOrEmpty(inheritedStyle))
                writer.AddAttribute(HtmlTextWriterAttribute.Style, inheritedStyle);
            writer.RenderBeginTag(HtmlTextWriterTag.Div); //Parent Div Control Begin Tag


            writer.AddAttribute(HtmlTextWriterAttribute.Id, "captchaImage");
            if (string.IsNullOrEmpty(_captchaImageCSS))
                writer.AddAttribute(HtmlTextWriterAttribute.Style, "margin:5px;float:left;");
            else
                writer.AddAttribute(HtmlTextWriterAttribute.Class, _captchaImageCSS);

            //Render Span and Image tag for Captcha
            writer.RenderBeginTag(HtmlTextWriterTag.Span);//Captcha Image span begin tag
            writer.Write(GenerateCaptchaImageTag(captchaString));
            writer.RenderEndTag();//Captcha Image span end tag

            writer.AddAttribute(HtmlTextWriterAttribute.Id, "captchaInput");
            if (string.IsNullOrEmpty(_captchaMessageCSS))
                writer.AddAttribute(HtmlTextWriterAttribute.Style, "margin:5px;float:left;");
            else
                writer.AddAttribute(HtmlTextWriterAttribute.Class, _captchaMessageCSS);
            //Render Span and Input tag for Captcha
            writer.RenderBeginTag(HtmlTextWriterTag.Span);//Captcha Input span begin tag

            writer.WriteEncodedText("Enter text below :");
            writer.WriteBreak();

            writer.AddAttribute(HtmlTextWriterAttribute.Name, UniqueID);
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "text");
            writer.AddAttribute(HtmlTextWriterAttribute.Size, captchaString.Length.ToString());
            writer.AddAttribute(HtmlTextWriterAttribute.Maxlength, captchaString.Length.ToString());
            writer.AddAttribute(HtmlTextWriterAttribute.Value, string.Empty);
            if (!string.IsNullOrEmpty(_captchaInputCSS))
                writer.AddAttribute(HtmlTextWriterAttribute.Class, _captchaInputCSS);

            writer.RenderBeginTag(HtmlTextWriterTag.Input);
            writer.RenderEndTag();//Captcha Input control end tag

            writer.RenderEndTag();//Captcha Input span end tag
            writer.RenderEndTag();//Parent Div Control End Tag
        }

        protected override object SaveControlState()
        {
            return _captchaId;
        }

        protected override void LoadControlState(object savedState)
        {
            if (savedState != null)
            {
                _prevCaptchaId = (string)savedState;
            }
        }

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            Page.RegisterRequiresControlState(this);
            Page.Validators.Add(this);
        }

        protected override void OnUnload(EventArgs e)
        {
            if ((Page != null))
            {
                Page.Validators.Remove(this);
            }
            base.OnUnload(e);
        }

        protected override void OnPreRender(EventArgs e)
        {
            if (Visible)
            {
                InitializeCaptcha();
            }
            base.OnPreRender(e);
        }

        #endregion

        #region Captcha management functions
        /// <summary>
        /// Initialize captcha control
        /// 1. Generate a new captcha.
        /// 2. Store it in the ASP.NET Session by unique GUID
        /// </summary>
        private void InitializeCaptcha()
        {
            //Skip in VS design mode, if in design mode HttpContext.Current is null
            if (!IsDesignMode)
            {
                // Create catpcha string to draw.
                Random r = new Random();
                int startIndex = r.Next(1, 5);
                int length = r.Next(_captchaTextMinLength, _captchaTextMaxLength);
                String captchaString = Guid.NewGuid().ToString("N").ToUpper().Substring(startIndex, length);
                HttpContext.Current.Session.Add(_captchaId, captchaString);
            }
        }
        
        /// <summary>
        /// Validate the entered captcha text with sent data
        /// </summary>
        /// <param name="userInputForCaptcha">Input text entered by user</param>
        private void ValidateEnteredCaptcha(string userInputForCaptcha)
        {
            if (!Visible | !Enabled)
            {
                _captchaValidated = true;
                return;
            }

            //-- retrieve the previous captcha from the cache to inspect its properties
            string captchaString = GetCachedCaptcha(_prevCaptchaId);
            if (string.IsNullOrEmpty(captchaString))
            {
                ErrorMessage = "The code you typed has expired";
                _captchaValidated = false;
                return;
            }

            if (string.Compare(userInputForCaptcha, captchaString, StringComparison.CurrentCultureIgnoreCase) != 0)
            {
                ErrorMessage = "The code you typed does not match the code in the image.";
                _captchaValidated = false;
                RemoveCachedCaptcha(_prevCaptchaId);
                return;
            }

            _captchaValidated = true;
            RemoveCachedCaptcha(_prevCaptchaId);
        }

        /// <summary>
        /// Retrieve a cached captcha by unique id
        /// </summary>
        /// <param name="captchaUniqueId">Unique id of cached captcha</param>
        /// <returns></returns>
        private string GetCachedCaptcha(string captchaUniqueId)
        {
            return Convert.ToString(HttpContext.Current.Session[captchaUniqueId]);
        }

        /// <summary>
        /// Remove cached captcha from Cache
        /// </summary>
        /// <param name="captchaUniqueId">Unique id of cached captcha</param>
        private void RemoveCachedCaptcha(string captchaUniqueId)
        {
            HttpContext.Current.Session.Remove(captchaUniqueId);
        }

        #endregion

        #region Captcha Image functions

        private string GenerateCaptchaImageTag(string captchaString)
        {
            using (Bitmap bitmap = new Bitmap(_captchaImageWidth, _captchaImageHeight, PixelFormat.Format32bppArgb))
            {
                using (Graphics graphics = Graphics.FromImage(bitmap))
                {
                    Rectangle outerRect = new Rectangle(0, 0, _captchaImageWidth -1 , _captchaImageHeight - 1);
                    graphics.FillRectangle(Brushes.White, outerRect);

                    /* CAPTCHA DRAWING WITH IMAGE DEFORM METHOD
                    using (var graphicsPath = new GraphicsPath())
                    {
                        FontFamily fontFamily = new FontFamily(GetFontName());
                        graphicsPath.AddString(captchaString, fontFamily, (int)FontStyle.Bold, 24f, new Point(0, 0), StringFormat.GenericTypographic);

                        using (var deformGraphicPath = DeformCaptchaImage(graphicsPath, _captchaImageWidth, _captchaImageHeight))
                        {
                            var bounds = deformGraphicPath.GetBounds();
                            var translationMatrix = new Matrix();
                            var x = (_captchaImageWidth - bounds.Width) / 2 - bounds.Left;
                            var y = (_captchaImageHeight - bounds.Height) / 2 - bounds.Top;
                            translationMatrix.Translate(x,y);
                            deformGraphicPath.Transform(translationMatrix);
                            graphics.FillPath(Brushes.Black, deformGraphicPath);
                        }
                    }*/

                    /* CAPTCHA DRWAING WITH CHARACTER ROTATION DEFORM */
                    // Calculate width for each character.
                    int charWidth = (int)(_captchaImageWidth / captchaString.Length);
                    for (int ctr = 0; ctr < captchaString.Length; ctr++)
                    {
                        using (Font fontForChar = new Font(_allowedFontList[ctr % (_allowedFontList.Count -1)], 24f, FontStyle.Bold))
                        {
                            DrawCharacter(captchaString.Substring(ctr, 1),
                                        graphics,
                                        fontForChar,
                                        ctr * charWidth,
                                        charWidth,
                                        _captchaImageWidth,
                                        _captchaImageHeight);
                        }
                    }
                    return GetImageTagWithBase64Data(bitmap, ImageFormat.Jpeg);
                }
            }
        }

        private void DrawCharacter(string charToDraw, 
            Graphics graphicObj,
            Font fontForChar, 
            int xValue, 
            int charWidth, 
            int captchaWidth, 
            int height)
        {
            // Center the text.
            StringFormat sf = new StringFormat();
            sf.Alignment = StringAlignment.Center;
            sf.LineAlignment = StringAlignment.Center;
            RectangleF rectf = new RectangleF(xValue, 0, charWidth, height);

            // Convert the text into a path.
            using (GraphicsPath gp = new GraphicsPath())
            {
                gp.AddString(charToDraw, fontForChar.FontFamily,
                    (int)(fontForChar.Style), fontForChar.Size, rectf, sf);

                // Make random warping parameters.
                Random randomizer = new Random();
                float x1 = (float)(xValue + randomizer.Next(charWidth) / 2);
                float y1 = (float)(randomizer.Next(height) / 2);
                float x2 = (float)(xValue + charWidth / 2 + randomizer.Next(charWidth) / 2);
                float y2 = (float)(height / 2 + randomizer.Next(height) / 2);

                PointF[] points = {
                    new PointF((float)(xValue + randomizer.Next(charWidth) / 4), (float)(randomizer.Next(height) / 4)),
                    new PointF((float)(xValue + charWidth - randomizer.Next(charWidth) / 4), (float)(randomizer.Next(height) / 4)),
                    new PointF((float)(xValue + randomizer.Next(charWidth) / 4), (float)(height - randomizer.Next(height) / 4)),
                    new PointF((float)(xValue + charWidth - randomizer.Next(charWidth) / 4), (float)(height - randomizer.Next(height) / 4))
                };

                Matrix mat = new Matrix();
                gp.Warp(points, rectf, mat, WarpMode.Perspective, 0);

                // Rotate a bit randomly.
                float dx = (float)(xValue + charWidth / 2);
                float dy = (float)(height / 2);

                graphicObj.TranslateTransform(-dx, -dy, MatrixOrder.Append);
                int angle = angleForPreviousChar;
                do
                {
                    angle = randomizer.Next(-30, 30);
                } while (Math.Abs(angle - angleForPreviousChar) < 20);

                angleForPreviousChar = angle;
                graphicObj.RotateTransform(angle, MatrixOrder.Append);
                graphicObj.TranslateTransform(dx, dy, MatrixOrder.Append);

                // Draw the text.
                graphicObj.FillPath(Brushes.Black, gp);
                graphicObj.ResetTransform();
            }
        }    

        private GraphicsPath DeformCaptchaImage(GraphicsPath path,  int width, int height)
        {
            Random randomizer = new Random();
            var WarpFactor = 3;
            var xAmp = WarpFactor * width / 300d;
            var yAmp = WarpFactor * height / 50d;
            var xFreq = 2d * Math.PI / width;
            var yFreq = 2d * Math.PI / height;
            var deformed = new PointF[path.PathPoints.Length];
            var xSeed = randomizer.NextDouble() * 2 * Math.PI;
            var ySeed = randomizer.NextDouble() * 2 * Math.PI;
            var i = 0;
            foreach (var original in path.PathPoints)
            {
                var val = xFreq * original.X + yFreq * original.Y;
                var xOffset = (int)(xAmp * Math.Sin(val + xSeed));
                var yOffset = (int)(yAmp * Math.Sin(val + ySeed));
                deformed[i++] = new PointF(original.X + xOffset,
                                         original.Y + yOffset);
            }

            return new GraphicsPath(deformed,
                                    path.PathTypes);
        }

        private string GetImageTagWithBase64Data(Bitmap bitmap, ImageFormat imageFormat)
        {
            string strImgTag = string.Empty;
            string strBase64 = string.Empty;

            strBase64 = GetBase64StringFromImage(bitmap, imageFormat);

            strImgTag = "<img src=\"data:image/" + imageFormat.ToString() + ";base64,";
            strImgTag += strBase64 + "\" ";
            strImgTag += "width=\"" + bitmap.Width.ToString() + "\" ";
            strImgTag += "height=\"" + bitmap.Height.ToString() + "\" />";

            return strImgTag;
        }

        private string GetBase64StringFromImage(Bitmap bitmap, ImageFormat imageFormat)
        {
            string strBase64 = string.Empty;

            MemoryStream bufferMemoryStream = new MemoryStream();
            bitmap.Save(bufferMemoryStream, imageFormat);

            bufferMemoryStream.Position = 0;
            byte[] byteBuffer = bufferMemoryStream.ToArray();

            bufferMemoryStream.Close();

            strBase64 = Convert.ToBase64String(byteBuffer);
            byteBuffer = null;

            return strBase64;
        }

        #endregion

        #region Utility methods

        private string GetFontName()
        {
            Random rnd = new Random();
            int fontNameIndex = rnd.Next(0, _allowedFontList.Count - 1);
            return _allowedFontList[fontNameIndex];
        }

        private string GetStyleTag()
        {
            StringBuilder sbStyleTagBuilder = new StringBuilder();
            sbStyleTagBuilder.Append(" style='");

            if (BorderWidth.ToString().Length > 0)
            {
                sbStyleTagBuilder.Append("border-width:");
                sbStyleTagBuilder.Append(BorderWidth.ToString());
                sbStyleTagBuilder.Append(";");
            }
            if (BorderStyle != BorderStyle.NotSet)
            {
                sbStyleTagBuilder.Append("border-style:");
                sbStyleTagBuilder.Append(BorderStyle.ToString());
                sbStyleTagBuilder.Append(";");
            }

            string strColor = GetHtmlColorCode(BorderColor);
            if (strColor.Length > 0)
            {
                sbStyleTagBuilder.Append("border-color:");
                sbStyleTagBuilder.Append(strColor);
                sbStyleTagBuilder.Append(";");
            }

            strColor = GetHtmlColorCode(BackColor);
            if (strColor.Length > 0)
            {
                sbStyleTagBuilder.Append("background-color:" + strColor + ";");
            }

            strColor = GetHtmlColorCode(ForeColor);
            if (strColor.Length > 0)
            {
                sbStyleTagBuilder.Append("color:" + strColor + ";");
            }

            if (Font.Bold)
            {
                sbStyleTagBuilder.Append("font-weight:bold;");
            }

            if (Font.Italic)
            {
                sbStyleTagBuilder.Append("font-style:italic;");
            }

            if (Font.Underline)
            {
                sbStyleTagBuilder.Append("text-decoration:underline;");
            }

            if (Font.Strikeout)
            {
                sbStyleTagBuilder.Append("text-decoration:line-through;");
            }

            if (Font.Overline)
            {
                sbStyleTagBuilder.Append("text-decoration:overline;");
            }

            if (Font.Size.ToString().Length > 0)
            {
                sbStyleTagBuilder.AppendFormat("font-size:{0};", Font.Size);
            }

            if (Font.Names.Length > 0)
            {
                sbStyleTagBuilder.Append("font-family:");
                foreach (string strFontFamily in Font.Names)
                {
                    sbStyleTagBuilder.Append(strFontFamily);
                    sbStyleTagBuilder.Append(",");
                }
                sbStyleTagBuilder.Length = sbStyleTagBuilder.Length - 1;
                sbStyleTagBuilder.Append(";");
            }

            if (!String.IsNullOrEmpty(Height.ToString()))
            {
                sbStyleTagBuilder.Append("height:" + Height + ";");
            }

            if (!String.IsNullOrEmpty(Width.ToString()))
            {
                sbStyleTagBuilder.Append("width:" + Width + ";");
            }

            sbStyleTagBuilder.Append("'");

            if (sbStyleTagBuilder.ToString() == " style=''")
            {
                return String.Empty;
            }
            return sbStyleTagBuilder.ToString();
        }

        private static string GetHtmlColorCode(Color inputColor)
        {
            if (inputColor.IsEmpty)
                return String.Empty;
            //Get Color by Name
            if (inputColor.IsNamedColor)
            {
                return inputColor.ToKnownColor().ToString();
            }
            //Check is system Color
            if (inputColor.IsSystemColor)
            {
                return inputColor.ToString();
            }
            //Otherwise get RGB code and return
            return "#" + inputColor.ToArgb().ToString("x", CultureInfo.CurrentCulture).Substring(2);
        }
        #endregion
    }
}
